# Working copy viewer.
#
# Displays the available actions to perform on the working copy.

import pysvn
from Tkinter import *
from tkMessageBox import *
from Queue import Queue, Empty
import threading
from threading import Thread
from util import do_nothing
from status_ui import *
from login_ui import LoginUI
from commit_ui import CommitUI
from shelf_ui import ShelfUI
from pick_branch_ui import PickBranchUI
import os, sys, re

CHANGE_INTERVAL = 200

class WorkingCopyItemInfo(object):
  def __init__(self, title, info, actions=(), color="black"):
    self.title = title
    self.info = info
    self.actions = actions
    self.color = color

STATUS_INFO = {
    pysvn.wc_status_kind.none: WorkingCopyItemInfo(
      "No Status",
      "Does not exist",
      color="#808080"),
    pysvn.wc_status_kind.unversioned: WorkingCopyItemInfo(
      "Unversioned",
      "Not a versioned file in this working copy.",
      ('add', 'ignore'),
      color="#FF8000"),
    pysvn.wc_status_kind.normal: WorkingCopyItemInfo(
      "No Changes",
      "This item exists in the Repository, and you have not changed it.",
      ('delete',),
      color="#808080"),
    pysvn.wc_status_kind.added: WorkingCopyItemInfo(
      "Added",
      "This item will be added to the Repository when you commit.",
      ('revert', 'delete', 'ignore'),
      "#808080"),
    pysvn.wc_status_kind.missing: WorkingCopyItemInfo(
      "Missing",
      "This item is in the Repository, but not in your working copy",
      ('restore', 'delete'),
      "#FF0000"),
    pysvn.wc_status_kind.deleted: WorkingCopyItemInfo(
      "Deleted",
      "This item will be deleted from the Repository when you commit.  However, you can still access older versions of it from the Repository history!",
      ('revert',),
      "#800000"),
    pysvn.wc_status_kind.replaced: WorkingCopyItemInfo(
      "Replaced",
      "This item was deleted, then added back again.",
      ('revert', 'delete'),
      "#800080"),
    pysvn.wc_status_kind.modified: WorkingCopyItemInfo(
      "Modified",
      "You modified this item, and your changes will end up in the Repository when you commit.",
      ('revert', 'delete'),
      "#800080"),
    pysvn.wc_status_kind.merged: WorkingCopyItemInfo(
      "Merged",
      "When you last updated, your local changes were merged with changes in the Repository.  This might not do what you expect, so you should check on this file...",
      ('revert', 'mine'),
      "#808000"),
    pysvn.wc_status_kind.conflicted: WorkingCopyItemInfo(
      "Conflicted",
      "When you last updated, your local changes conflicted with someone else's changes.  You'll need to resolve this conflict before committing.",
      ('revert', 'mine', 'theirs', 'resolved'),
      "#FF0000"),
    pysvn.wc_status_kind.ignored: WorkingCopyItemInfo(
      "Ignored",
      "You have chosen not to include this file in your Repository.",
      ('add',),
      "#808080"),
    pysvn.wc_status_kind.obstructed: WorkingCopyItemInfo(
      "Obstructed",
      "You have an unversioned resource 'in the way' of a versioned resource.  This can happen if you delete a folder without using EasySVN, then add it back again.",
      (),
      "#FF0000"),
    pysvn.wc_status_kind.external: WorkingCopyItemInfo(
      "External",
      "This path is connected to an external SVN repository.",
      (),
      "#000080"),
    pysvn.wc_status_kind.incomplete: WorkingCopyItemInfo(
      "Incomplete",
      "This directory does not have a complete list of entries.",
      (),
      "#008080")
}

DEFAULT_BENCH_TEXT = """Here are the items in your working copy.
Click on one for more info, and to act on it."""

class WorkingCopyBench(object):
  "Displays working copy item information."
  def __init__(self, master, wc):
    #TODO: init location.
    self.wc = wc
    frame = self.frame = Frame(master)
    self.frame.pack(side=TOP, fill=X)
    self.lbl = Label(frame, text=DEFAULT_BENCH_TEXT, wraplength=400, height=3)
    self.lbl.pack(side=TOP, fill=X, expand=1)
    self.actions = {
        'add': Button(frame, text='Add', command=self.addItem),
        'ignore': Button(frame, text='Ignore', command=self.ignoreItem),
        'delete': Button(frame, text='Delete', command=self.deleteItem),
        'revert': Button(frame, text='Revert', command=self.revertItem),
        'mine': Button(frame, text='Mine', command=self.useMyItem),
        'theirs': Button(frame, text='Theirs', command=self.useTheirItem),
        'resolved': Button(frame, text='Resolved', command=self.resolveItem),
      }
    for action in self.actions.itervalues():
      action.pack(side=LEFT)
      action.config(state=DISABLED)

  def get_responder(self, title):
    return StatusUI(self.frame, title, do_nothing)

  def addItem(self):
    self.wc.addItem(self.item.path, self.get_responder('Adding item...'))

  def ignoreItem(self):
    self.wc.ignoreItem(self.item.path, self.get_responder("Ignoring item..."))

  def deleteItem(self):
    self.wc.deleteItem(self.item.path, self.get_responder("Deleting item..."))

  def revertItem(self):
    self.wc.revertItem(self.item.path, self.get_responder("Reverting item..."))

  def useMyItem(self):
    self.wc.useMyItem(self.item.path, self.get_responder("Using my version..."))

  def useTheirItem(self):
    self.wc.useTheirItem(self.item.path, self.get_responder("Using their version..."))

  def resolveItem(self):
    self.wc.resolveItem(self.item.path, self.get_responder("Marking conflict resolved..."))

  # TODO: don't use camelcase above
  def bind_item(self, item=None):
    "Connects a working copy item to this panel, or reverts if item is None"
    self.item = item
    for button in self.actions.itervalues():
      button.config(state=DISABLED)
    if item:
      if item.status_code is not None:
        info = STATUS_INFO[item.status_code]
        print "Actions: %s" % (info.actions,)
        for action in info.actions:
          self.actions[action].config(state=NORMAL)
        self.lbl.configure(text=info.info)
      else:
        self.lbl.configure(text="This item has no info.")
      # TODO: allow people to add/remove folders
    else:
      self.lbl.configure(text=DEFAULT_BENCH_TEXT)


  def on_pick(self, item):
    pass

# TODO: document the other classes.

class WorkingCopyItem(object):
  def __init__(self, title):
    self.status = None
    self.children = {}
    self.title = title

  def set_status(self, status):
    self.status_code = status.text_status
    self.path = status.path
    self.has_entry = (status.entry is not None)
    self.is_dir = False
    if self.has_entry:
      self.is_dir = (status.entry.kind == pysvn.node_kind.dir)
    else:
      # This only happens for unversioned paths, so it's probably
      # not so bad for performance.
      self.is_dir = os.path.isdir(self.path)

  def add_child(self, child):
    self.children[child.title] = child

  def get_child(self, title):
    return self.children.get(title)

  def get_or_make_child(self, title):
    child = self.get_child(title)
    if child is None:
      self.children[title] = child = WorkingCopyItem(title)
    return child

  def accept(self, visitor):
    visitor.push_path(self.title)
    visitor.visit(self)
    for title, child in self.children.iteritems():
      child.accept(visitor)
    visitor.pop_path()

class WorkingCopyFiles(object):
  def __init__(self, statuses, wc):
    self.wc = wc
    self.statuses = statuses
    self.root = WorkingCopyItem(os.path.basename(wc.path))

    for status in statuses:
      self.add_path(status.path[len(self.wc.path):], status)

  def find_item(self, path):
    path_items = path.split(os.sep)
    item = self.root
    for path_item in path_items:
      if not path_item:
        continue
      item = item.get_child(path_item)
      if item is None:
        return None
    return item

  def add_path(self, path, status):
    path_items = path.split(os.sep)

    # Create all paths up to the given path.
    item = self.root
    for path_item in path_items:
      if not path_item:
        continue # skip blank entries
      item = item.get_or_make_child(path_item)

    # Save the path's status
    item.set_status(status)

  def get_root(self):
    return self.root

class WorkingCopyViewVisitor(object):
  def __init__(self, lst):
    self.lst = lst
    self.paths = []
    self.index_to_item = []

  # TODO: stop working with paths....
  def push_path(self, item):
    self.paths.append(item)

  def pop_path(self):
    self.paths.pop()

  def get_index_to_item(self):
    return self.index_to_item

  def visit(self, item):
    # TODO: title depends on path?
    # TODO: skipping the first path is a little hacky...
    text = "   " * (len(self.paths) - 1) + item.title

    # TODO: only use singleton styles
    # Show status codes only for unversioned directories
    # or for any files.
    if item.status_code is not None and \
      (not item.has_entry or not item.is_dir):
      status_info = STATUS_INFO[item.status_code]
      text += " (%s)" % status_info.title

    self.index_to_item.append(item)
    self.lst.insert(END, text)

class WorkingCopyList(object):
  def __init__(self, master, wc):
    self.frame = frame = Frame(master)
    self.frame.pack(side=LEFT, fill=BOTH, expand=1)
    # TODO: does the bench belong here?
    self.bench = WorkingCopyBench(frame, wc)
    vscroll = Scrollbar(frame)
    vscroll.pack(side=RIGHT, fill=Y)
    self.file_list = Listbox(frame, width=40, yscrollcommand=vscroll.set)
    self.file_list.pack(fill=BOTH, expand=1)
    vscroll.config(command=self.file_list.yview)
    self.wc = wc
    self.files = None

    self.selection = ()
    self.check_for_change()
    self.index_to_item = None

  def update_paths(self, statuses):
    self.file_list.delete(0, END)
    self.files = WorkingCopyFiles(statuses, self.wc)
    visitor = WorkingCopyViewVisitor(self.file_list)
    self.files.get_root().accept(visitor)
    self.index_to_item = visitor.get_index_to_item()

  def check_for_change(self):
    "Poll for changes to selection."
    selection = self.file_list.curselection()
    if selection != self.selection:
      print selection
      self.selection = selection
      if selection and self.index_to_item:
        self.bench.bind_item(self.index_to_item[int(selection[0])])
      else:
        self.bench.bind_item(None)
    self.frame.after(CHANGE_INTERVAL, self.check_for_change)

class WorkingCopyCmd(object):
  def __init__(self, parent, text, info, callback):
    self.btn = Button(parent, text=text, command=callback)
    self.btn.pack(side=TOP, fill=X, expand=1)
    self.label = Label(parent, text=info, wraplength=300)
    self.label.pack(side=TOP, fill=X, expand=1)

class WorkingCopyView(object):
  def __init__(self, wc):
    self.wc = wc
    self.working_copy = wc # TODO: dedup
    self.master = Tk()
    self.master.title("Your Working Copy - EasySVN")

    self.frame = Frame(self.master)
    # TODO: separate editor object from the main window?
    self.cmds = [
      WorkingCopyCmd(self.frame, "Commit", "Share your changes with your team, by putting them into the Repository.",
        self.commit),
      WorkingCopyCmd(self.frame, "Update", "Get the latest changes from your team.",
        self.update),
      WorkingCopyCmd(self.frame, "Revert All", "Undo ALL of your changes, going back to the most recent version in the Repository.",
        self.revert),
      WorkingCopyCmd(self.frame, "Emergency Commit", "Stash your changes in the Repository, without merging them into the project.  Should only be used if you don't have time to perform a regular Commit.",
        self.shelf),
      WorkingCopyCmd(self.frame, "Apply Emergency Commit", "Merge changes from an emergency commit.", self.merge)
    ]
    self.frame.pack(side=LEFT, anchor=N)
    self.wc_list = WorkingCopyList(self.master, wc)

    # TODO: a better way to handle this?
    # Set the status flag on working copy update, so we know to update the
    # working copy status in the Tkinter main loop thread.
    # TODO: only update paths that change.
    self.status_flag = threading.Event()
    self.wc.on_change = self.on_change
    self.status()

  def on_change(self):
    self.status_flag.set()

  def run(self):
    # TODO: consolidate this with the WorkingCopyList call.
    # TODO: have a better transfer of control from the last window to this one
    self.master.after(CHANGE_INTERVAL, self.check_for_change)
    self.master.mainloop()

  def check_for_change(self):
    if self.status_flag.is_set():
      self.status_flag.clear()
      self.status()
    self.master.after(CHANGE_INTERVAL, self.check_for_change)

  def add_files(self):
    pass

  def remove_files(self):
    pass

  def status(self):
    "Get the repository status."
    # TODO
    # Determine status and display results in the status view.
    print "Calculating status..."

    def finish_status():
      self.wc_list.update_paths(statuses)

    reporter = StatusUI(self.master, "Checking status...", finish_status)

    def do_status(reporter):
      global statuses
      reporter.put("Calculating status...")
      statuses = self.working_copy.status()
      reporter.put(EXIT)
      return

    thread = Thread(target=do_status, args=(reporter,))
    thread.daemon = True
    thread.start()

  def on_path_change(self, path):
    "Update the status of the file."
    # TODO: does this belong here?
    pass

  def revert(self):
    "Discards changes to the given path."
    # This is a per-file operation.
    # Warn user that their changes will be lost for the checked files.
    pass

  def diff_titles(self):
    "Determines the changes at file level."
    # TODO
    # Ask what revision to diff against.

    # Display the differences in a window.

    pass

  def commit(self):
    "Commits changes to the Repository."
    # TODO: Check status of repository.
    # If repository is on a different
    # version, user will have to Update or Commit to Private Branch

    # TODO: Display window with commit message + changes
    # TODO: make sure the commit message isn't empty.
    # TODO: make sure changes were made!

    def continue_commit(message):
      responder = StatusUI(self.master, "Commit Status", do_nothing)
      # TODO: provide responder as part of commit arguments?
      thread = Thread(target=self.wc.commit, args=(message, responder))
      thread.daemon = True
      thread.start()

    CommitUI(self.master, continue_commit)

    # OPTIONAL: Task tracker?

  def shelf(self):
    "Saves changes to the Repository."
    # TODO

    # Create new branch and do the emergency commit.
    # TODO: make sure changes were made!
    def continue_shelf(message, chosen_branch):
      responder = StatusUI(self.master, "Emergency Commit Status", do_nothing)
      thread = Thread(target=self.wc.shelve, args=(chosen_branch, message, responder))
      thread.daemon = True
      thread.start()

    # Display window with commit message + changes
    ShelfUI(self.master, continue_shelf)

  def merge(self):
    "Merges another branch with the current one."

    # Perform merge
    def on_choose(chosen_branch):
      responder = StatusUI(self.master, "Merge Status", do_nothing)
      # TODO match up argument order?
      thread = Thread(target=self.wc.unshelve, args=(chosen_branch, responder))
      thread.daemon = True
      thread.start()

    def on_branches_listed():
      # Ask for other branch
      PickBranchUI(self.master, branches, on_choose)

    responder = StatusUI(self.master, "Getting Emergency Commit List", \
      on_branches_listed, show_list = False)

    # Get list of branches
    def list_branches():
      global branches
      responder.put('Getting list of commits...')
      branches = self.working_copy.repository.get_branch_list(responder)

    t = Thread(target=list_branches)
    t.daemon = True
    t.start()

  def update(self):
    "Updates the working copy to the given revision."
    # TODO
    # Show changes that will be updated, with option to update to a
    # different revision.

    # Also show log messages, so you know what changed.

    # Perform update if user wants it.

    # TODO: Display window with commit message + changes
    # TODO: make sure the commit message isn't empty.
    # TODO: make sure changes were made!
    responder = StatusUI(self.master, "Update Status", do_nothing)

    thread = Thread(target=self.wc.update, args=(responder,))
    thread.daemon = True
    thread.start()
